//
//  SMTPMailDelivery.m
//  MailDelivery
//
//  Created by Dante Palacios on 30/08/09.
//  Copyright 2009 Dante Palacios. All rights reserved.
//

#import <AppKit/NSImage.h>
#import <AddressBook/AddressBook.h>

#import "SMTPMailDelivery.h"
#import "TCPConnection.h"
#import "Message.h"
#import "Keychain.h"

#import "NSArrayAdditions.h"
#import "NSAttributedStringAdditions.h"
#import "NSDictionaryAdditions.h"
#import "NSStringAdditions.h"

#import "Constants.h"
#import "Errors.h"
#import "Funtions.h"
#import "Macros.h"


NSString *const MailDeliveryCompletedNotification = @"MailDeliveryCompletedNotification";
NSString *const MailDeliveryFailedNotification = @"MailDeliveryFailedNotification";

@interface SMTPMailDelivery ()

- (BOOL)_makeConnectionWithHosts:(NSArray *)servers onPorts:(NSArray *)ports error:(NSError **)error;
- (BOOL)_deliverMessage:(Message *)message error:(NSError **)error;

@end

@implementation SMTPMailDelivery

@dynamic connection;
@dynamic deliveryAccount;

#pragma mark Life Cycle

- (id)init 
{
	self = [super init];
	if (self != nil) 
	{
		_userCanceled = NO;
	}
	return self;
}

- (id)initWithDeliveryAccount:(NSDictionary *)account 
{
	SMTPMailDelivery *mailDelivery = [[[self class] alloc] init];
	mailDelivery.deliveryAccount = account;
	return mailDelivery;
}

- (void)dealloc 
{
	[_deliveryAccount release];
	_deliveryAccount = nil;
	[_connection release];
	_connection = nil;
	
	[super dealloc];
}

#pragma mark public methods

- (void)cancel 
{
	_userCanceled = YES;
	[self.connection cancelLastRequest];
}

- (BOOL)deliverMessage:(Message *)message error:(NSError **)error 
{
	_userCanceled = NO;
	if (![self _deliverMessage:message error:error]) 
	{
		[[NSNotificationCenter defaultCenter] performSelectorOnMainThread:@selector(postNotification:) withObject:[NSNotification notificationWithName:MailDeliveryFailedNotification object:self userInfo:[NSDictionary dictionaryWithObject:message forKey:@"message"]] waitUntilDone:NO];
		return NO;
	}
	
	[[NSNotificationCenter defaultCenter] performSelectorOnMainThread:@selector(postNotification:) withObject:[NSNotification notificationWithName:MailDeliveryCompletedNotification object:self userInfo:[NSDictionary dictionaryWithObject:message forKey:@"message"]] waitUntilDone:NO];
	return YES;
}

- (BOOL)verifyDeliveryAccountAndReturnError:(NSError **)error; 
{
	if (!_deliveryAccount) return NO;
	
	NSString *hostName = self.deliveryAccount.hostName;
	if (!hostName.length)
	{
		if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError  userInfo:[NSDictionary dictionaryWithObject:MDLocalizedString(@"SMTP server requiered", @"") forKey:NSLocalizedDescriptionKey]];
		return NO;
	}
	
	NSString *userName = self.deliveryAccount.userName;
	if (!userName.length) 
	{
		if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError userInfo:[NSDictionary dictionaryWithObject:MDLocalizedString(@"Username requiered", @"") forKey:NSLocalizedDescriptionKey]];
		return NO;
	}
	
	// deal with ports
	NSArray *defaultPorts = self.deliveryAccount.usesDefaultPorts ? [NSArray arrayWithObjects:[NSNumber numberWithInteger:587], [NSNumber numberWithInteger:465], [NSNumber numberWithInteger:25], nil] : [NSArray arrayWithObject:[NSNumber numberWithInteger:self.deliveryAccount.port]];
	
	// make a list of servers
	// we can do a lot more here…
	NSMutableArray *servers = [NSMutableArray array];
	[servers addObject:hostName];
	
	// make the connection
	
	if (![self _makeConnectionWithHosts:servers onPorts:defaultPorts error:error]) return NO;
	
	NSString *password = self.deliveryAccount.password;
	NSInteger authentication = self.deliveryAccount.authentication;
	if (!password && (authentication != kAuthenticationNone)) 
	{
		if (!(password = KeychainGetPasswordForAccount(self.deliveryAccount))) 
		{
			if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:MDLocalizedString(@"Password Required", @""), NSLocalizedDescriptionKey, MDLocalizedString(@"Delivery account requires authentication, but no password has been specified.", @""), NSLocalizedRecoverySuggestionErrorKey, nil]];
			
			return NO;
		}
	}
	
	if (![self.connection autenthicateWithHost:hostName userName:userName password:password scheme:authentication]) 
	{
		if (error) *error = self.connection.error;
		return NO;
	}
	
	return YES;		
}

+ (NSDictionary *)deliveryAccountForEmailAddress:(NSString *)emailAddress 
{
	NSArray *deliveryAccounts = [[self class] deliveryAccounts];
	if (!deliveryAccounts.count) return nil;
	
	NSDictionary *account = nil;
	if (!emailAddress.length) 
	{
		account = [deliveryAccounts objectAtIndex:0];
	}
	
	if (!account) 
	{
		NSPredicate *predicate = [NSPredicate predicateWithFormat:@"EmailAddresses contains %@", emailAddress];
		NSArray *filteredArray = [deliveryAccounts filteredArrayUsingPredicate:predicate];
		if (filteredArray.count)
		{
			account = [filteredArray objectAtIndex:0];
		}
	}
	
	if (!account) 
	{
		MailDeliveryLog(@"Warning!, delivery account for email address \"%@\" not found…", emailAddress);
	}
	
	return account;
}

#pragma mark private methods

- (BOOL)_deliverMessage:(Message *)message error:(NSError **)error 
{
	if (!message.recipients.count) 
	{
		if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError userInfo:[NSDictionary dictionaryWithObject:MDLocalizedString(@"This message has no recipients", @"") forKey:NSLocalizedDescriptionKey]];
		return NO;
	}
	
	id <MessageAddressee> sender = message.sender;
	NSString *fromEmail = [sender address];
	
	if (!_deliveryAccount) // use the instance variable first…
	{
		MailDeliveryLog(@"Warning!, sending email without delivery account…");
		self.deliveryAccount = [[self class] deliveryAccountForEmailAddress:fromEmail];
		if (!self.deliveryAccount) // call to our lazy getter
		{
			if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError userInfo:[NSDictionary dictionaryWithObject:MDLocalizedString(@"Unable to find a delivery account", @"") forKey:NSLocalizedDescriptionKey]];
			return NO;
		}
		
		MailDeliveryLog(@"Delivery account for email address \"%@\" found…", fromEmail);
	}
	
	// sync sender with delivery account
	NSArray *emailAddresses = self.deliveryAccount.emailAddresses;
	if (![emailAddresses containsObject:fromEmail]) 
	{
		if (!(sender = (id <MessageAddressee>)self.deliveryAccount.defaultSender)) 
		{
			if (error) *error = [NSError errorWithDomain:MailDeliveryErrorDomain code:kMailDeliveryWrongArgumentsError userInfo:[NSDictionary dictionaryWithObject:MDLocalizedString(@"This message has no sender", @"") forKey:NSLocalizedDescriptionKey]];
			
			return NO;
		}
		
		//NSLog(@"Assigning new sender to the message…");
		
		// assing the new sender to the message
		NSMutableDictionary *newMessageHeaders = [NSMutableDictionary dictionary];
		[newMessageHeaders addEntriesFromDictionary:message.headers];
		[newMessageHeaders setObject:sender forKey:@"From"];
		
		message.headers = newMessageHeaders;
		
		fromEmail = [sender address];
	}
	
	if (![self verifyDeliveryAccountAndReturnError:error]) return NO;
	
	NSString *mailTo = [message.recipients componentsJoinedAsRecipients];
	if (![self.connection writeEnvelopeTo:mailTo from:fromEmail]) 
	{
		if (error) *error = self.connection.error;
		return NO;
	}
	
	self.connection.uses8BITMIME = (message.encoding != NSASCIIStringEncoding);
	
	if (![self.connection writeData:message.source]) 
	{
		if (error) *error = self.connection.error;
		return NO;
	}
	
	message.dateSent = [NSDate date];
	
	[self.connection invalidate];
	
	return YES;	
}

- (BOOL)_makeConnectionWithHosts:(NSArray *)servers onPorts:(NSArray *)ports error:(NSError **)error 
{
	NSString *host = nil;
	NSEnumerator *enumer = [servers objectEnumerator];
	while((host = [enumer nextObject])) 
	{
		if (_userCanceled) break;
		
		for (NSNumber *currentPort in ports) {
			if (_userCanceled || [self.connection connectToHost:host port:[currentPort integerValue]]) break;
		}
	}
	
	if (_userCanceled) [self.connection cancelLastRequest];
	
	if (!self.connection.isValid) 
	{
		if (error) *error = self.connection.error;
		
		return NO;
	}
	
	self.connection.securityLevel = self.deliveryAccount.SSLSecurityLevel;
	
	return YES;
}

#pragma mark Accesors

- (TCPConnection *)connection 
{
	TCPConnection *value = nil;
	@synchronized(self)
	{
		if (!_connection) 
		{
			_connection = [[TCPConnection alloc] init];
			_connection.connectionTimeout = 8.0;
			_connection.provideDetailedConnectionError = NO;
		}
		
		value = [[_connection retain] autorelease];
	}
	
	return value;
}

- (void)setConnection:(TCPConnection *)value 
{
    AtomicRetainedSet(_connection, value);
}

- (NSDictionary *)deliveryAccount 
{
	NSDictionary *value = nil;
	@synchronized(self) 
	{
		if (!_deliveryAccount) 
		{
			// lets try to get the email account from Mail.app
			NSArray *deliveryAccounts = [[self class] deliveryAccounts];
			if (deliveryAccounts.count) 
			{
				MailDeliveryLog(@"Using default account…");
				_deliveryAccount = [[deliveryAccounts objectAtIndex:0] retain];
			}
			else {
				// lets try to create a new account from the email addresses of the current user, for each email addresses, lets look for an entry in Keychain
				MailDeliveryLog(@"Getting smtp account from Address Book…");
				
				NSString *host = nil;
				NSString *user = nil;
				NSString *pass = nil;
				NSString *emailAddress = nil;
				
				ABPerson *person = [[ABAddressBook sharedAddressBook] me];
				ABMultiValue *emailAddresses = [person valueForProperty:kABEmailProperty];
				NSUInteger i, count = [emailAddresses count];
				for (i = 0; i < count; i++) 
                {
					emailAddress = [emailAddresses valueAtIndex:i];
					host = emailAddress.hostName;
					user = emailAddress.userName;
                    if (!host || !user) continue;
                    
					NSDictionary *tempAccount = [NSDictionary dictionaryWithObjectsAndKeys:host, kDeliveryAccountHostName, user, kDeliveryAccountUserName, nil];
					
					if ((pass = KeychainGetPasswordForAccount(tempAccount))) break;
				}
                
				if (emailAddress.length && host.length && user.length) 
				{
					// well done!, we have an account to send our message
					NSMutableDictionary *account = [NSMutableDictionary dictionary];
					[account setObject:NSFullUserName() forKey:kDeliveryAccountFullUserName];
					[account setObject:[NSArray arrayWithObject:emailAddress] forKey:kDeliveryAccountEmailAddresses];
					[account setObject:host forKey:kDeliveryAccountHostName];
					[account setObject:user forKey:kDeliveryAccountUserName];
					
					if (pass)
						[account setObject:pass forKey:kDeliveryAccountPassword];
					else
						// try to send the message without password...
						[account setObject:[NSNumber numberWithInteger:kAuthenticationNone] forKey:kDeliveryAccountAuthentication];
					
					MailDeliveryLog(@"Using account for email address \"%@\"…", emailAddress);
					
					_deliveryAccount = [account retain];			
				}
			}					
		}
		value = [[_deliveryAccount retain] autorelease];
	}
		
	return value;
}

- (void)setDeliveryAccount:(NSDictionary *)value 
{
     AtomicRetainedSet(_deliveryAccount, value);
}

+ (NSArray *)mailAppDeliveryAccounts 
{
	NSFileManager *fm = [[[NSFileManager alloc] init] autorelease];
	NSDictionary *mailPreferences = nil;
	SInt32 osVersion;
	
	Gestalt(gestaltSystemVersion, &osVersion);
	if (osVersion >= 0x1070)
	{
		NSURL *libraryURL = [fm URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:NULL];
		NSURL *mailDataURL = [[[libraryURL URLByAppendingPathComponent:@"Mail"] URLByAppendingPathComponent:@"V2"] URLByAppendingPathComponent:@"MailData"];
		NSURL *accountsURL = [mailDataURL URLByAppendingPathComponent:@"Accounts.plist"];
		
		if (![fm fileExistsAtPath:accountsURL.path])
			return nil;
		mailPreferences = [NSDictionary dictionaryWithContentsOfURL:accountsURL];
	}
	else
	{
		NSString *prefsPath = [[[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Preferences"] stringByAppendingPathComponent:@"com.apple.mail.plist"];
		
		if (![fm fileExistsAtPath:prefsPath]) return nil;
		
		mailPreferences = [NSDictionary dictionaryWithContentsOfFile:prefsPath];
	}
	
	NSArray *deliveryAccounts = [mailPreferences valueForKey:@"DeliveryAccounts"];
	NSArray *mailAccounts = [mailPreferences valueForKey:@"MailAccounts"];
	if (!deliveryAccounts.count) return nil;
	
	NSMutableArray *accounts = [NSMutableArray array];
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	for (NSDictionary *mailAccount in mailAccounts) 
	{
		NSArray *smtpIdentifierComponents = [[mailAccount objectForKey:@"SMTPIdentifier"] componentsSeparatedByString:@":"];
		if (!smtpIdentifierComponents || smtpIdentifierComponents.count < 2) continue;
		
		NSString *hostName = [smtpIdentifierComponents objectAtIndex:0];
		NSString *userName = [smtpIdentifierComponents objectAtIndex:1];
		NSArray *filteredArray = [deliveryAccounts filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"Hostname == %@ AND Username == %@", hostName, userName]];
		if (!filteredArray || filteredArray.count == 0) continue;
		
		NSDictionary *mailDeliveryAccount = [filteredArray objectAtIndex:0UL];
		
		NSArray *emailAddresses = [mailAccount objectForKey:kDeliveryAccountEmailAddresses];
		if (!emailAddresses.count) continue;
		
		NSString *fullName = [mailAccount objectForKey:kDeliveryAccountFullUserName];
		if (!fullName) fullName = @"";
		
		NSString *accountName = [mailAccount objectForKey:kDeliveryAccountName];
		if (!accountName.length) accountName = [emailAddresses componentsJoinedByString:@", "];
		
		NSNumber *portNumber = [mailDeliveryAccount objectForKey:kDeliveryAccountPort];
		NSInteger port = portNumber ? [portNumber integerValue] : 587;
		
		NSMutableDictionary *account = [NSMutableDictionary dictionary];
		[account setObject:accountName forKey:kDeliveryAccountName];
		[account setObject:userName forKey:kDeliveryAccountUserName];
		[account setObject:hostName forKey:kDeliveryAccountHostName];
		[account setObject:fullName forKey:kDeliveryAccountFullUserName];
		[account setObject:emailAddresses forKey:kDeliveryAccountEmailAddresses];
		[account setObject:[NSNumber numberWithInteger:kAuthenticationPlain] forKey:kDeliveryAccountAuthentication];
		[account setObject:[NSNumber numberWithInteger:port] forKey:kDeliveryAccountPort];
		[account setObject:[NSNumber numberWithBool:YES] forKey:kDeliveryAccountUsesDefaultPorts];
		[account setObject:[NSNumber numberWithBool:YES] forKey:kDeliveryAccountUsesSSL];
		[account setObject:[NSNumber numberWithInteger:MailDeliverySecurityLevelTLSv1] forKey:kDeliveryAccountSSLSecurityLevel];
		
		[accounts addObject:account];			
	}
	[pool drain];
	
	return accounts;
}

+ (NSArray *)deliveryAccounts 
{
	NSMutableArray *accounts = [NSMutableArray array];
	NSArray *smtpAccounts = [[NSUserDefaults standardUserDefaults] arrayForKey:@"SMTPAccounts"];
	NSArray *mailAppAccounts = [self mailAppDeliveryAccounts];
	
	if (smtpAccounts)
		[accounts addObjectsFromArray:smtpAccounts];
	if (mailAppAccounts)
		[accounts addObjectsFromArray:mailAppAccounts];
	
	return accounts;
}

+ (NSImage *)genericEmailAccountIconImage 
{
	return [[[NSImage alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[SMTPMailDelivery class]] pathForImageResource:@"GenericAccount"]] autorelease];
}

@end

@implementation SMTPMailDelivery(SMTPMailDeliveryStandardAdditions) 

+ (BOOL)deliverMessage:(NSAttributedString *)messageBody headers:(NSDictionary *)messageHeaders format:(NSString *)messageFormat error:(NSError **)error 
{
	SMTPMailDelivery *mailDelivery = [[[self class] alloc] init];
	Message *message = [[Message alloc] initWithHeaders:messageHeaders content:messageBody format:messageFormat];
	BOOL mesageSent = NO;
	if ([mailDelivery deliverMessage:message error:error]) 
	{
		mesageSent = YES;
	}
	
	[message release];
	[mailDelivery release];
	
	return mesageSent;
}

+ (BOOL)deliverMessage:(NSString *)messageBody subject:(NSString *)messageSubject to:(NSString *)destinationAddress error:(NSError **)error 
{
	if (!messageBody) messageBody = @"";
	if (!messageSubject) messageSubject = @"";
	
	NSAttributedString *content = [[[NSAttributedString alloc] initWithString:messageBody] autorelease];
	NSDictionary *messageHeaders = [NSDictionary dictionaryWithObjectsAndKeys:messageSubject, @"Subject", destinationAddress, @"To", nil];
	return [self deliverMessage:content headers:messageHeaders format:kPlainTextMessageFormat error:error];
}

@end